1 线程管理基础笔记¶
参考资料地址:https://chenxiaowei.gitbook.io/cpp_concurrency_in_action/ 并发编程有两种模型,多进程(每个进程有一个线程,进程间相互通信的方式有文件,管道,消息队列等),多线程(一个个进程有多个线程,线程间以共享内存方式进行通信). 多线程 优点:1启动快,2开销小,性能优越。 缺点:1难于管理,逻辑控制复杂,2无法在分布式系统下运行。 多进程 优点: 1空间独立,数据安全,2创建方便 缺点: 1开销大,2频繁创建和删除较多进程的情况下,资源消耗过多,不适宜使用多进程完成任务(可用进程池技术解决)。
启动线程的方法(构造std::thread对象): std::thread my_thread(A); 这个A是为线程指定的任务。A是无参数无返回的函数对象或仿函数对象—-即传入将带有函数调用符类型的实例(即重载了函数调用符()的类的实例)。 两个例子: void do_some_work(); std::thread my_thread(do_some_work);
class background_task { public: void operator()() const { do_something(); do_something_else(); } };
background_task f; std::thread my_thread(f);
note: 当把函数对象传入到线程构造函数中时,需要避免“最令人头痛的语法解析”(C++’s most vexing parse, 中文简介)。如果你传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。 例如: std::thread my_thread(background_task()); 这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了一个线程。 使用在前面命名函数对象的方式,或使用多组括号①,或使用新统一的初始化语法②,可以避免这个问题。 如下所示: std::thread my_thread((background_task())); // 1 std::thread my_thread{background_task()}; // 2 使用lambda表达式也能避免这个问题。lambda表达式是C++11的一个新特性,它允许使用一个可以捕获局部变量的局部函数(可以避免传递参数,参见2.2节)。想要具体的了解lambda表达式,可以阅读附录A的A.5节。之前的例子可以改写为lambda表达式的类型: std::thread my_thread([]{ do_something(); do_something_else(); });
两种错误: 1 未使用detach方法与join方法: joinable()返回true的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached,不然会报abort错误,程序就会终止(std::thread的析构函数会调用std::terminate()). (调用join()的行为,还清理了线程相关的存储部分,这样std::thread对象将不再与已经完成的线程有任何关联。这意味着,只能对一个线程使用一次join();一旦已经使用过join(),std::thread对象就不能再次加入了,当对其使用joinable()时,将返回false。)
2 线程访问另一个已经被销毁的线程的局部变量,会产生危险的未定义行为。 处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。如果使用一个可调用的对象作为线程函数,这个对象就会复制到线程中,而后原始对象就会立即销毁。但对于对象中包含的指针和引用还需谨慎,例如使用一个能访问局部变量的函数去创建线程是一个糟糕的主意(除非十分确定线程会在函数完成前结束(通过join()函数来确保线程在函数完成前结束))。 清单2.1 struct func { int& i; func(int& i_) : i(i_) {} void operator() () { for (unsigned j=0 ; j<1000000 ; ++j) { do_something(i); // 1. 潜在访问隐患:悬空引用 } } };
void oops() { int some_local_state=0; func my_func(some_local_state); std::thread my_thread(my_func); my_thread.detach(); // 2. 不等待线程结束 } // 3. 新线程可能还在运行
等待线程完成: join()是简单粗暴的等待线程完成或不等待。当你需要对等待中的线程有更灵活的控制时,比如,看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时)。想要做到这些,你需要使用其他机制来完成,比如条件变量和期待(futures)
特殊等待线程完成: 当在线程运行之后产生异常,在join()调用之前抛出,就意味着这次调用会被跳过。甚至会出现上述两种错误中的第2类情形。 避免应用被抛出的异常所终止,就需要作出一个决定。 方法1 通常,当倾向于在无异常的情况下使用join()时,需要在异常处理过程中调用join(),从而避免生命周期的问题。 简而言之用try包含线程创建后的语句块,在join语句前用catch块捕获join并抛出。 方法2使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),并且提供一个类,在析构函数中使用join(),如同下面清单中的代码。 class thread_guard { std::thread& t; public: explicit thread_guard(std::thread& t_): t(t_) {} ~thread_guard() { if(t.joinable()) // 1 { t.join(); // 2 } } thread_guard(thread_guard const&)=delete; // 3 thread_guard& operator=(thread_guard const&)=delete; };
struct func; // 定义在清单2.1中
void f() { int some_local_state=0; func my_func(some_local_state); std::thread t(my_func); thread_guard g(t); do_something_in_current_thread(); } // 4 代码解析: 当线程执行到④处时,局部对象就要被逆序销毁了。因此,thread_guard对象g是第一个被销毁的,这时线程在析构函数中被加入②到原始线程中。即使do_something_in_current_thread抛出一个异常,这个销毁依旧会发生。 在thread_guard的析构函数的测试中,首先判断线程是否已加入①,如果没有会调用join()②进行加入。这很重要,因为join()只能对给定的对象调用一次,所以对给已加入的线程再次进行加入操作时,将会导致错误。 拷贝构造函数和拷贝赋值操作被标记为=delete③,是为了不让编译器自动生成它们。直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。想要了解删除函数的更多知识,请参阅附录A的A.2节。 如果不想等待线程结束,可以分离(detaching)线程,从而避免异常安全*(exception-safety)问题。不过,这就打破了线程与std::thread对象的联系,即使线程仍然在后台运行着,分离操作也能确保std::terminate()在std::thread对象销毁才被调用。
后台运行线程: 使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束; 如果线程分离,那么就不可能有std::thread对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。 C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。 通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,发后即忘(fire and forget)的任务就使用到线程的这种方式。 额外知识note: 线程的分离状态决定一个线程以什么样的方式来终止自己。在默认情况下线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。所以如果我们在创建线程时就知道不需要了解线程的终止状态,则可以pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。
疑问(待解决): 1,2.1.1中有这样一段话”代码中,提供的函数对象会复制到新线程的存储空间当中,函数对象的执行和调用都在线程的内存空间中进行。函数对象的副本应与原始函数对象保持一致,否则得到的结果会与我们的期望不同。” 何时函数对象的副本与原始函数对象会出现不一致呢? 2,notes中的例子std::thread my_thread(background_task()); 为什么传入了一个类名加括号?(我的理解是(background_task()调用了该类的构造函数,会返回一个临时类的实例对象,但为什么资料里说”这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了一个线程。”这里和我理解的偏差在于函数指针众所周知是不能指向构造函数的。) 3,后台运行线程中提到了”另一方面,分离线程的另一方面只能确定线程什么时候结束”,是只能确定还是不能确定?一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。那如何确定线程结束的时间呢?是有函数现在还未学习到么。